{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Breaking Hardware ECC on CW305 FPGA, part 4"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This builds on CW305_ECC parts 1, 2 and 3 notebooks; be sure to digest them before starting this one.\n",
    "\n",
    "In this notebook, we study a fourth and final modifications to the target Verilog source code to reduce the side-channel leakage.\n",
    "\n",
    "This modification will reveal some side-channel leakage which had not be noted previously.\n",
    "\n",
    "The tutorial was developed with a CW-Pro with the CW305 100t target FPGA; the observations made in the attack's development should be accurate if you're using the same, but other combinations of CW-Pro / CW-Lite / CW-Husky / CW305 100t / 35t / CW312T-A35 may behave somewhat differently (some definitely do!)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Setup\n",
    "\n",
    "See CW305_ECC_part1.ipynb for explanations which are not repeated here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#PLATFORM = 'CWLITE'\n",
    "#PLATFORM = 'CWPRO'\n",
    "PLATFORM = 'CWHUSKY'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#TARGET_PLATFORM = 'CW305_100t'\n",
    "#TARGET_PLATFORM = 'CW305_35t'\n",
    "TARGET_PLATFORM = 'CW312T_A35'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "TRACES = 'HARDWARE' # if you have the required capture+target hardware: capture actual traces\n",
    "#TRACES = 'SIMULATED' # if you don't have capture+target hardware: use pre-captured traces (these traces were obtained using CW-Husky with a  CW305_100t)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import chipwhisperer as cw\n",
    "import time\n",
    "\n",
    "if TRACES != 'SIMULATED':\n",
    "    scope = cw.scope()\n",
    "    scope.default_setup()\n",
    "    if TARGET_PLATFORM == 'CW312T_A35':\n",
    "        scope.io.hs2 = 'clkgen'\n",
    "        fpga_id = 'cw312t_a35'\n",
    "        platform = 'ss2'\n",
    "    else:\n",
    "        scope.io.hs2 = \"disabled\"\n",
    "        platform = 'cw305'\n",
    "        if TARGET_PLATFORM == 'CW305_100t':\n",
    "            fpga_id = '100t'\n",
    "        elif TARGET_PLATFORM == 'CW305_35t':\n",
    "            fpga_id = '35t'\n",
    "\n",
    "    target = cw.target(scope, cw.targets.CW305_ECC, force=False, fpga_id=fpga_id, platform=platform)\n",
    "    \n",
    "    # ensure ADC is locked:\n",
    "    time.sleep(0.5)\n",
    "    scope.clock.reset_adc()\n",
    "    assert (scope.clock.adc_locked), \"ADC failed to lock\"\n",
    "\n",
    "%run \"CW305_ECC_setup.ipynb\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Occasionally the ADC will fail to lock on the first try; when that happens, the above assertion will fail (and on the CW-Lite, the red LED will be on). Simply re-running the above cell again should fix things."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Attempt #4\n",
    "\n",
    "While attempt #3 was shown to be effective, we will now compare it with a much more expensive approach: a complete duplication of the target core.\n",
    "\n",
    "Since the side-channel leakage originates from the differences when a bit of $k$ is 0 or 1, and since the time required to process each bit of $k$ is always the same, it stands to reason that instantiating a second copy of the target core which processes the inverse of $k$ in parallel with (at the same time as) the original core could also be an effective (albeit expensive!) countermeasure."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "change_bitfile('attempt4')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "k = 0xffffffffffffffffffffffffffffffff00000000000000000000000000000000\n",
    "traces = get_traces(1, k, 'part4_1', full=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's begin by looking at the raw difference between ones and zeros, as we did for the other attempts:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "samples = 4204\n",
    "trace = traces[0]\n",
    "avg_ones = np.zeros(samples)\n",
    "for start in cycles[1:128]:\n",
    "    avg_ones += trace.wave[start:start+samples]\n",
    "avg_ones /= 128\n",
    "\n",
    "avg_zeros = np.zeros(samples)\n",
    "for start in cycles[128:256]:\n",
    "    avg_zeros += trace.wave[start:start+samples]\n",
    "avg_zeros /= 128"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bokeh.plotting import figure, show\n",
    "from bokeh.resources import INLINE\n",
    "from bokeh.io import push_notebook, output_notebook\n",
    "from ipywidgets import interact, Layout\n",
    "\n",
    "output_notebook(INLINE)\n",
    "s = figure(width=2000)\n",
    "\n",
    "xrange = list(range(len(avg_ones)))\n",
    "s.line(xrange, avg_ones - avg_zeros, line_color=\"orange\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "show(s)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is a **drastically** different picture from the one we are used to!\n",
    "\n",
    "Let's again compare it to the leakage from the original target bitfile:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "change_bitfile('original')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "otraces = get_traces(1, k, 'part1_1', full=True)\n",
    "otrace = otraces[0]\n",
    "\n",
    "oavg_ones = np.zeros(samples)\n",
    "for start in cycles[1:128]:\n",
    "    oavg_ones += otrace.wave[start:start+samples]\n",
    "oavg_ones /= 128\n",
    "\n",
    "oavg_zeros = np.zeros(samples)\n",
    "for start in cycles[128:256]:\n",
    "    oavg_zeros += otrace.wave[start:start+samples]\n",
    "oavg_zeros /= 128"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bokeh.models import Legend, LegendItem\n",
    "\n",
    "diff = figure(width=2000)\n",
    "\n",
    "odiff = oavg_ones - oavg_zeros\n",
    "newdiff = avg_ones - avg_zeros\n",
    "\n",
    "xrange = list(range(len(newdiff)))\n",
    "O = diff.line(xrange, odiff, line_color=\"black\")\n",
    "N = diff.line(xrange, newdiff, line_color=\"orange\")\n",
    "\n",
    "legend = Legend(items=[\n",
    "    LegendItem(label='original 0/1 difference', renderers=[O]),\n",
    "    LegendItem(label='new 0/1 difference', renderers=[N]),\n",
    "])\n",
    "diff.add_layout(legend)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "show(diff)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Zooming in around cycles 6 and 4202 shows that leakage still peaks around those clock cycles, but now we find numerous other peaks in addition to the original peaks.\n",
    "\n",
    "If we amplify the original leakage, we find that the new leakage observed with attempt 4 actually also appears with attempt 1; it's just these new leakage points are much weaker in the original bitfile:\n",
    "\n",
    "(This is evident if you zoom in around many of the larger orange peaks, for example cycles 1600 or 3100.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "diff = figure(width=2000)\n",
    "\n",
    "scale = np.max(newdiff) / np.max(odiff[100:4000])\n",
    "\n",
    "xrange = list(range(len(newdiff)))\n",
    "diff.line(xrange, odiff*scale, line_color=\"black\")\n",
    "diff.line(xrange, newdiff, line_color=\"orange\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "show(diff)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's visualize the DoM distinguisher, first using only our original DoM markers (cycles 6, 7, 4202 and 4203):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "change_bitfile('attempt4')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "k = 0x0000ffffffffff000000000000ffff00aaaa0000cccc00001111000033330000\n",
    "traces = get_traces(30, k, 'part4_2', full=False, samples_per_segment=64)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "poi = [4202, -4203, -6, 7]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def update_corrected_plot(no_traces):\n",
    "    SSC.data_source.data['y'] = get_corrected_sums(traces[:no_traces], poi)\n",
    "    push_notebook()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "SC = figure(width=1200, x_axis_label='k bit index', y_axis_label='D')\n",
    "\n",
    "xrange = list(range(len(cycles)-1))\n",
    "sums = get_corrected_sums(traces[:15], poi)\n",
    "SSC = SC.line(xrange, sums, line_color='black')\n",
    "SC.xaxis.axis_label_text_font_size = '20pt'\n",
    "SC.yaxis.axis_label_text_font_size = '20pt'\n",
    "SC.xaxis.major_label_text_font_size = '14pt'\n",
    "SC.yaxis.major_label_text_font_size = '14pt'\n",
    "SC.title.text_font_size = '20pt'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "show(SC, notebook_handle=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "interact(update_corrected_plot, no_traces=(1, len(traces)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Visually, this does seem worse than the original target.\n",
    "\n",
    "We'll go through our usual attack, but before we get to that let's see what happens if we add the newly identified leakage markers. There seem to be *a lot* of these so let's use an automated process to extract them.\n",
    "\n",
    "Let's re-center our 1 vs 0 plot, and pick `POS_THRESHOLD` and `NEG_THRESHOLD` as the minimum thresholds for selecting the largest peaks for our new list of markers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bokeh.models import Span\n",
    "\n",
    "if TARGET_PLATFORM == 'CW312T_A35':\n",
    "    POS_THRESHOLD = 0.025\n",
    "    NEG_THRESHOLD = -0.030\n",
    "else:\n",
    "    POS_THRESHOLD = 0.045\n",
    "    NEG_THRESHOLD = -0.045\n",
    "    \n",
    "# in case samples were recorded as ints, translate result to make it as though they were floats, so that the *THRESHOLDS can cover both cases:\n",
    "if 'int' in str(type(traces[0].wave[0])):\n",
    "    if PLATFORM == 'CWPRO':\n",
    "        div = 2**10\n",
    "    # infer whether trace was collected with 8 or 12 bits per sample:\n",
    "    elif max(abs(traces[0].wave)) > 255:\n",
    "        div = 2**12\n",
    "    else:\n",
    "        div = 2**8\n",
    "else:\n",
    "    div = 1\n",
    "\n",
    "diff = figure(width=2000)\n",
    "\n",
    "xrange = list(range(len(avg_ones)))\n",
    "diff.line(xrange, (newdiff - np.average(newdiff))/div, line_color=\"red\")\n",
    "\n",
    "pos_threshold = Span(location=POS_THRESHOLD, dimension='width', line_color='black')\n",
    "neg_threshold = Span(location=NEG_THRESHOLD, dimension='width', line_color='black')\n",
    "diff.renderers.extend([pos_threshold, neg_threshold])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "show(diff)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The default values for `POS_THRESHOLD` and `NEG_THRESHOLD` should work well for CW-Husky with a CW305_100t or CW312T-A35 target; you may need to adjust them if you end up with too few or too many markers, but this is not an exact science."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "avg = (newdiff - np.average(newdiff))/div\n",
    "poi = list(np.where(avg > POS_THRESHOLD)[0]) + list(-np.where(avg < NEG_THRESHOLD)[0])\n",
    "\n",
    "assert len(poi) > 100 and len(poi) < 400, \"Got %d markers; goal is >100 and <400. Tweak POS_THRESHOLD and NEG_THRESHOLD until this passes.\" % len(poi)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "if TRACES == 'HARDWARE':\n",
    "    num_traces = 30\n",
    "else:\n",
    "    num_traces = 1\n",
    "# need to acquire a full trace to use all the POIs; we can show some results with a single trace:\n",
    "traces = get_traces(num_traces, k, 'part4_3', full=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bokeh.models import Label\n",
    "\n",
    "SCnew = figure(width=1200, x_axis_label='k bit index', y_axis_label='D')\n",
    "\n",
    "xrange = list(range(len(cycles)-1))\n",
    "sums = get_corrected_sums([traces[0]], poi)\n",
    "SSCnew = SCnew.line(xrange, sums, line_color='black')\n",
    "SCnew.xaxis.axis_label_text_font_size = '20pt'\n",
    "SCnew.yaxis.axis_label_text_font_size = '20pt'\n",
    "SCnew.xaxis.major_label_text_font_size = '14pt'\n",
    "SCnew.yaxis.major_label_text_font_size = '14pt'\n",
    "SCnew.title.text_font_size = '20pt'\n",
    "\n",
    "k_text = Label(x=5, y=-10, text='k = {16 zeros, 40 ones, ...}')\n",
    "\n",
    "SCnew.add_layout(k_text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "show(SCnew)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is a staggering result: from a **single trace**, we get an astonishingly clear distinguisher which coincides with the location of the leading one bit of $k$.\n",
    "\n",
    "The source of this new leakage can be readily found with a quick look at a simulation waveform (return to part 1 for how to do this): we find that intermediate results written to the `bram_rz` target memory are 256’d1 (255 zeros followed by a single 1) as long as the target is processing leading zeros; when the first 1 is encountered, data written to `bram_rz` changes to random-looking data with a Hamming weight of around 128.\n",
    "\n",
    "Go back and play with different values of $k$ to confirm that this is what is happening.\n",
    "\n",
    "In the original core, only the leading one is leaked -- you can confirm this by re-running the capture above with the original bitfile (but keeping the new expanded list of markers).\n",
    "\n",
    "However with attempt #4, the leading zero is **also** leaked (since that ends up being the leading one for the second core). **This is a great example for the unintended consequences of countermeasures!**\n",
    "\n",
    "None of the other bits of $k$ are leaked from this marker, so on the surface these new markers may appear less useful than the ones we had used until now. But the result above suggests that this marker could be 100% reliable at finding the leading 0 and 1 from a single trace, which avoids having to deal with bad guesses and the throwing away of guesses for which we do not have sufficient confidence.\n",
    "\n",
    "So it's possible that an improved, better attack could be build from this. But for the sake of finishing our comparison with our three other countermeasure attempts (and the original design), we will continue here with our original attack.\n",
    "\n",
    "Before doing so, let's have a closer look at the DoM components. Skip this if you're using pre-recorded traces, since these have been omitted due to space constraints:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "if TRACES == 'HARDWARE':\n",
    "    poicomponents = figure(width=1200, x_axis_label='k bit index', y_axis_label='D')\n",
    "\n",
    "    xrange = list(range(len(cycles)-1))\n",
    "    poi = [4202, -4203, -6, 7]\n",
    "\n",
    "    sumsall = get_corrected_sums(traces, [4202, -4203, -6, 7])\n",
    "    sumscomp = get_corrected_sums(traces, [-4202, 4203, -6, 7])\n",
    "    sums6 = get_corrected_sums(traces, [-6, 7])\n",
    "    sums4202 = get_corrected_sums(traces, [4202, -4203])\n",
    "\n",
    "    poicomponents.line(xrange, sumsall, line_color='black')\n",
    "    poicomponents.line(xrange, sumscomp, line_color='red')\n",
    "    poicomponents.line(xrange, sums6, line_color='blue', line_width=2)\n",
    "    poicomponents.line(xrange, sums4202, line_color='orange')\n",
    "    poicomponents.xaxis.axis_label_text_font_size = '20pt'\n",
    "    poicomponents.yaxis.axis_label_text_font_size = '20pt'\n",
    "    poicomponents.xaxis.major_label_text_font_size = '14pt'\n",
    "    poicomponents.yaxis.major_label_text_font_size = '14pt'\n",
    "    poicomponents.title.text_font_size = '20pt'\n",
    "\n",
    "    show(poicomponents)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This plot shows the countermeasure's effectiveness: while the leakage at cycles 6-7 is preserved (blue curve), the leakage at cycles 4202-4203 (orange curve) now only reveals the leading 1.\n",
    "\n",
    "If we combine all the markers (black curve), the leading 1 markers effectively cancel each other out. This could be addressed by complementing the 4202-4203 component, but this adds noise to the rest of the measurements (red curve).\n",
    "\n",
    "It appears we are better off using only the leakage from cycles 6-7 for our attack.\n",
    "\n",
    "Some small tweaks are required for our guessing methodology:\n",
    "1. there is now a single guessing threshold (not two)\n",
    "2. we can't guess the first bit: we will leave it unknown and end with 4 possible values of $k$ for each guess (instead of 2)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "if TRACES == 'SIMULATED':\n",
    "    traces = get_traces(30, k, 'part4_4', full=False, samples_per_segment=64)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "poi = [-6, 7]\n",
    "sums = get_corrected_sums(traces, poi)\n",
    "\n",
    "poi_init_threshold = None\n",
    "poi_reg_threshold = (np.average(sums[104:119]) - np.average(sums[56:103]))/2 + np.average(sums[56:103])\n",
    "\n",
    "print('threshold: %3.2f' % poi_reg_threshold)\n",
    "\n",
    "attempt4thresholds = [poi_init_threshold, poi_reg_threshold]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And we carry out our usual sanity check, to see that we correctly guess $k$ when multiple traces are averaged:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sums = get_corrected_sums(traces, poi)\n",
    "guess = poi_guess(sums, attempt4thresholds)\n",
    "print(\"DoM: %s\" % check_guess(guess, k)[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can finally see how many errors we make on single-trace attacks, on average:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "traces = get_traces(100, k, 'part4_5', randomize_k=True, full=False, samples_per_segment=64)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "wrong_bits = []\n",
    "for trace in traces:\n",
    "    sums = get_corrected_sums([trace], poi)\n",
    "    guess = poi_guess(sums, attempt4thresholds)\n",
    "    wrong_bits.append(check_guess(guess, trace.textin['k'])[1])\n",
    "\n",
    "print('Average wrong bits per trace: %f' % np.average(wrong_bits))\n",
    "print('Minimum wrong bits per trace: %f' % min(wrong_bits))\n",
    "print('Maximum wrong bits per trace: %f' % max(wrong_bits))\n",
    "\n",
    "attempt1_average_wrong_bits = np.average(wrong_bits)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Despite its flaws, this countermeasure is effective against our attack (which, remember, could be improved here!): the average number of wrong bits per single-trace guess should be almost as high as what we obtained with attempt #3 in part 3 of this series."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# The Hidden Number Problem\n",
    "\n",
    "As in part 3, we conclude by measuring the number of traces with sufficient and good consecutive guesses, using segmented traces for efficiency.\n",
    "\n",
    "In the interest of storage constraints, these traces are not saved, so you'll need the required hardware to run this."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "trace_segments = get_trace_segments(N=5000, poi=poi, randomize_k=True, husky_timed_segments=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "consecutives(trace_segments=trace_segments, poi=poi, distance_threshold=0.91, thresholds=attempt4thresholds)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Some adjustment on `threshold` may be required; you should find substantially fewer good traces compared to the original target results from part 2, but more than with attempt #3."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Conclusion\n",
    "\n",
    "In this part we learned more about the unintended effects of countermeasures.\n",
    "\n",
    "Attempt #4 is much more expensive than attempt #3, yet it performs less well against our attack, **and** it introduces additional leakage which could be leveraged by a different attack.\n",
    "\n",
    "In part 5 we'll take a look at what TVLA can tell us about our target."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
